import time
import struct
import random
import logging
import serial as pyserial

import schedule
import RPi.GPIO as GPIO
from apscheduler.schedulers.background import BackgroundScheduler
from django.shortcuts import render, HttpResponse
from django.core import serializers


from django.conf import settings
from .models import PMS5003S, DHTXX


logger = logging.getLogger(__name__)


def sleep_microsecond(t, c=7):
    # 微秒级延时函数， time.sleep(0.000010)微秒级误差大、见官方文档说明。
    # param c：时间补偿，在树莓派4b上测得执行一句time.time()需要7微秒，自己调整。
    start, end = 0, 0
    start = time.time()
    t = (t-7)/1000000
    while end-start < t:
        end = time.time()
    return start, end

def pms5003s_collect(request=None):
    """
    攀藤PMS5003粉尘甲醛二合一传感器，数据收集
    原理、驱动逻辑详见说明书
    """
    sensor = PMS5003S()
    if settings.PMS5003S['switch'] == 'ON':
        serial = None
        try:
            # 1. 连接树莓派串口硬件地址 "/dev/ttyAMA0"，注意此接口默认被蓝牙占用需要修改设置。
            serial = pyserial.Serial(settings.PMS5003S['device_serial_port'], 9600, timeout=2)
            # 2. 主动式传感器，每0.2s~0.8s一次性发送32字节，直接开始读取。缓冲区最大4kb。
            _temp_time = time.time()
            while serial.in_waiting <= 32:
                time.sleep(0.2)
                if time.time() - _temp_time > 2 and serial.in_waiting == 0:
                    sensor.error_message = 'nodata'
                    logger.error('超时无数据，rx针没有获取到数据，确保传感器已连接和串口设备"/dev/ttyAMA0"已禁用蓝牙占用。')
                    raise RuntimeError('超时')
            # 3. 读取数据
            # 传感器端已校验，回传数据100%正确，直接取
            # struct解包，2位起始+2帧长度+2*13组数据+2校验位=32字节
            data_bytes = serial.read(32)
            logger.debug(f'data_bytes {data_bytes}')
            if not data_bytes.startswith(b'\x42\x4d'):
                sensor.error_message = 'not_found_start_bit'
                logger.error('起始位不正确')
                raise RuntimeError('起始位不正确')

            data = struct.unpack(f'>' + 'b' * 2 + 'h' + 'h' * 13 + 'h', data_bytes)
            logger.debug(f'data {data}', )

            # 截取数据部分13组
            # 4. 赋值
            data = data[3:-1]
            sensor.pm1dot0_industry = data[0]
            sensor.pm2dot5_industry = data[1]
            sensor.pm10dot0_industry = data[2]
            sensor.pm1dot0_atmosphere = data[3]
            sensor.pm2dot5_atmosphere = data[4]
            sensor.pm10dot0_atmosphere = data[5]
            sensor.pm0dot3_quantity = data[6]
            sensor.pm0dot5_quantity = data[7]
            sensor.pm1dot0_quantity = data[8]
            sensor.pm2dot5_quantity = data[9]
            sensor.pm5dot0_quantity = data[10]
            sensor.pm10dot0_quantity = data[11]
            sensor.formaldehyde = data[12] / 1000
            sensor.state = True
            # 5. 持久化
            sensor.save()
            logger.debug(f"""
                PM2.5: {sensor.pm2dot5_atmosphere} 微克/立方米
                甲醛：{sensor.formaldehyde}  微克/立方米
                create_time: {sensor.create_time}      id: {sensor.id}
            """)
        except Exception as e:
            logger.error('错误：', e)
        finally:
            # 6. 关闭串口。
            serial.flushInput()
            serial.close()

    elif settings.PMS5003S['switch'] == 'FAKE':
        sensor.state = False
        logger.warning('Fake data generated.', time.time())
        # todo

    elif settings.PMS5003S['switch'] == 'OFF':
        sensor.state = False
        sensor.error_message = 'offline'
        logger.warning('传感器pms5003s被配置为离线')

    return HttpResponse('')


def dht11_collect(request=None):
    """
    原理详见奥松DHT11/DHT22说明书
    未封装的推荐并联上拉电阻
    严格按照说明书时序，否则无响应。DHT11/DHT22时序略有不同。
    被动式传感器，单总线，上位机先发信号，再接收。上电和进入激活状态需要10s。
    其它参考代码（水平很高） https://github.com/szazo/DHT11_Python
    """
    try:
        sensor = DHTXX()
        data = []
        # 数据线，单线双向。BCM4 pin7
        if settings.DHT11['pin_mode'] == 'BCM':
            GPIO.setmode(GPIO.BCM)
        pin = settings.DHT11['pin_sda']
        # 1. 传感器上电等待1s，稳定后才能响应命令
        time.sleep(1)

        # 2. 设置总线为输出。上位机释放握手信号。
        #   拉低电平18～30ms，注意严格遵守时间。释放总线即拉高。
        GPIO.setup(pin, GPIO.OUT)
        time.sleep(0.05)
        GPIO.output(pin, GPIO.LOW)
        time.sleep(0.020)
        GPIO.output(pin, GPIO.HIGH)
        # 3. 设置总线为输入。等待传感器回复。
        #    传感器回复83微妙低电平，接87微秒高电平。
        GPIO.setup(pin, GPIO.IN)
        _tmp_time = time.time()
        _iter_nums = 0
        while GPIO.input(pin) == GPIO.LOW:
            _iter_nums += 1
            if time.time() - _tmp_time > 0.001:
                sensor.error_message = 'UPPER_HANDSHAKE_PULL_DOWN'
                raise Exception('超时，传感器握手应答下拉电平超时，UPPER_HANDSHAKE_PULL_DOWN')
            pass
        if _iter_nums == 0:
            sensor.error_message = 'UPPER_HANDSHAKE_PULL_DOWN'
            raise Exception('未应答，传感器未开始应答 UPPER_HANDSHAKE_PULL_DOWN。检查前面握手问题或传感器损坏。')
        _tmp_time = time.time()
        while GPIO.input(pin) == GPIO.HIGH:
            if time.time() - _tmp_time > 0.001:
                sensor.error_message = 'UPPER_HANDSHAKE_PULL_UP'
                # 由于GPIO本身3.3v和上拉电阻的存在，此处报错的原因，可能是前面的握手有问题，可能是传感器损坏。
                raise Exception('超时，传感器握手应答下拉电平超时，UPPER_HANDSHAKE_PULL_UP。'
                                '检查前面握手问题或传感器损坏，导致一直处于高电平，跨过了上面的while判断。')
            pass
        # 4. 传感器应答成功并开始传输数据
        # 共5字节40位。
        # 此时状态开始为 'DATA_PULL_DOWN'
        # 取数据共40bit。注意返回的不是结果数字的二进制，而是模拟型号。
        # 二进制0，54微秒低电平+23～27微秒高电平；二进制1，低电平+68～74微秒高电平。
        # 判断方法一：比较高低电平时间比例(0.8为分界点)。判断方法二：低电平出现后125微秒后此时高电平则为1。
        # [注意]：time.sleep(0.000001)在微秒级不精确，导致略过数据周期，数据位取不全。但time.time()精度是够的，需要封装微秒级延迟函数。
        #       下面的代码前不应该加任何语句，例如data=[]或print('开始收集数据位')，些许的时间流逝导致数据位错一位。表现为温度值错误但每次稳定。
        #       逻辑部分精良精简代码，一条正常语句大约7微秒。
        for i in range(40):
            while GPIO.input(pin) == GPIO.LOW:
                # 由于上拉电阻的存在，此处测试时从未超时，精简代码
                continue
            _tmp_time = time.time()
            while GPIO.input(pin) == GPIO.HIGH:
                # 三分之一概率位传输不全
                if time.time() - _tmp_time > 0.001:
                    sensor.error_message = 'NOT_COLLECTED_ALL_DATA'
                    raise Exception('超时，传输数据上拉超时，未收集够数据位，请重试。原因：数据可能已传输完毕保持待机状态，代码需要精简优化。')
                    break
                continue
            high_time = time.time() - _tmp_time
            # 时间比值
            if high_time/0.000054 < 0.6:
                data.append(0)
            else:
                data.append(1)

        # 第二种思路
        # for i in range(40):  # 循环40次，接收温湿度数据
        #     a = time.time()
        #     while GPIO.input(pin) == 0:  # 一直循环至输入为高电平
        #         b = time.time()
        #         if (b - a) > 0.1:
        #             break
        #     sleep_microsecond(28)  # 延时28微秒
        #
        #     if GPIO.input(pin):  # 超过28微秒后判断是否还处于高电平
        #         data.append(1)  # 记录接收到的bit为1
        #
        #         a = time.time()
        #         while GPIO.input(pin):  # 一直循环至输入为低电平
        #             b = time.time()
        #             if (b - a) > 0.1:
        #                 break
        #     else:
        #         data.append(0)  # 记录接收到的bit为0

        # 5. 数据转换
        # data并不是字节流，不能直接struct.unpack()或bytes()方法，先拼成字符串，按字节切片，最后int('00001111', 2)转十进制。
        # dht11精度到整数，如果温度零下，小数位最高位位1（十进制128）。
        data = ''.join([str(i) for i in data])
        logger.debug(f'dht11 data {len(data)} {data}')
        humidity_integer = int(data[0:8], 2)
        humidity_decimal = int(data[8:16], 2)
        temperature_integer = int(data[16:24], 2)
        temperature_decimal = int(data[24:32], 2)
        check = int(data[32:40], 2)

        if humidity_integer + humidity_decimal + temperature_integer + temperature_decimal == check:
            # 正确数据
            sensor.state = True
            sensor.temperature = temperature_integer if not temperature_decimal >= 128 else -temperature_integer
            sensor.humidity = humidity_integer
            sensor.save()
            logger.debug(f"""
                校验成功。温度: {temperature_integer}.{temperature_decimal} 湿度：{humidity_integer}.{humidity_decimal}
                id: {sensor.id} create_time: {sensor.create_time} 
            """)
        else:
            # 几分之一概率校验位未通过
            sensor.state = False
            sensor.error_message = 'NOT_PASSED_VALIDATE'
            sensor.temperature = temperature_integer
            sensor.humidity = humidity_integer
            sensor.save()
            logger.debug(f"""
                未通过校验，NOT_PASSED_VALIDATE。
                温度: {temperature_integer} 湿度：{humidity_integer}
                id: {sensor.id} create_time: {sensor.create_time} 
            """)
    except Exception as e:
        logger.error(e)
    finally:
        GPIO.cleanup()
        # return HttpResponse('')


def pms5003s_api(request):
    sensor = PMS5003S.objects.filter(state=True).order_by('-create_time')[:1]
    return HttpResponse(serializers.serialize('json', sensor))


def dht11_api(request):
    sensor = DHTXX.objects.filter(state=True).order_by('-create_time')[:1]
    return HttpResponse(serializers.serialize('json', sensor))


def test(request):
    pass

# todo （用完后删除）临时为了适配旧前端页面展示效果
def temperature_now(request):
    dht11 = DHTXX.objects.filter(state=True).order_by('-create_time')[0]
    return HttpResponse(dht11.temperature)
def humidity_now(request):
    dht11 = DHTXX.objects.filter(state=True).order_by('-create_time')[0]
    return HttpResponse(dht11.humidity)

def pms5003s(request):
    name = request.GET.get('name')
    sensor = PMS5003S.objects.last()
    if name == 'pm1dot0':
        return HttpResponse(sensor.pm1dot0_atmosphere)
    elif name == 'pm2dot5':
        return HttpResponse(sensor.pm2dot5_atmosphere)
    elif name == 'pm10dot0':
        return HttpResponse(sensor.pm10dot0_atmosphere)
    elif name == 'particulate':
        data = list()
        data.append(sensor.pm0dot3_quantity)
        data.append(sensor.pm0dot5_quantity)
        data.append(sensor.pm1dot0_quantity)
        data.append(sensor.pm2dot5_quantity)
        data.append(sensor.pm5dot0_quantity)
        data.append(sensor.pm10dot0_quantity)
        return HttpResponse(data.__str__())
    elif name == 'formaldehyde':
        return HttpResponse(sensor.formaldehyde)
    else:
        pass



# 定时任务
# 选型：schedule库线程阻塞。django-apscheduler封装了后台过麻烦。django-crontab可以选择。这里简单场景选用apscheduler。
scheduler = BackgroundScheduler()
if settings.PMS5003S['switch'] == 'ON':
    scheduler.add_job(pms5003s_collect, 'interval', seconds=settings.PMS5003S['interval'])     # 每0.2~0.8s出数据
if settings.DHT11['switch'] == 'ON':
    scheduler.add_job(dht11_collect, 'interval', seconds=settings.PMS5003S['interval'])     # 每2s出数据

scheduler.start()
